using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using NMock2.Matchers;
using NMock2.Syntax;
using NMock2.Events;

namespace NMock2.Internal
{
	internal class InterfaceExpectationBuilder<T> : ExpectationBuilder, IMethodSyntax<T>, IArgumentMatcher
		where T : class
	{
		public InterfaceExpectationBuilder(string description, Matcher requiredCountMatcher, Matcher acceptedCountMatcher, object mockObject)
			: base(description, requiredCountMatcher, acceptedCountMatcher)
		{
			On(mockObject);
		}

		internal bool IsStub
		{
			get;
			set;
		}

		#region Method

		public IArgumentSyntax Method(Expression<Action<T>> expression)
		{
			string methodName = ((MethodCallExpression)expression.Body).Method.Name;
			Method(methodName);
			return this;
		}

		public IAutoArgumentSyntax<TProperty> Method<TProperty>(Expression<Func<T, TProperty>> expression)
		{
			string methodName = ((MethodCallExpression)expression.Body).Method.Name;
			Method(methodName);
			return new PropertyArgumentBuilder<TProperty>(this);
		}

		public ICommentSyntax MethodWith(Expression<Action<T>> expression)
		{
			ParseMethodExpression(expression.Body as MethodCallExpression);
			return this;
		}

		public IAutoActionSyntax<TProperty> MethodWith<TProperty>(Expression<Func<T, TProperty>> expression)
		{
			ParseMethodExpression(expression.Body as MethodCallExpression);
			return new PropertyArgumentBuilder<TProperty>(this);
		}

		public ICommentSyntax MethodWith<TProperty>(Expression<Func<T, TProperty>> expression, TProperty actualValue)
		{
			return MethodWith(expression).Will(Return.Value(actualValue));
		}

		private void ParseMethodExpression(MethodCallExpression expression)
		{
			string methodName = expression.Method.Name;
			IArgumentSyntax argumentSyntax = Method(methodName);

			object[] expectedArguments = ParseArguments(expression);

			argumentSyntax.With(expectedArguments);
		}

		private object[] ParseArguments(MethodCallExpression expression)
		{
			List<object> argsList = new List<object>();
			foreach (Expression argument in expression.Arguments)
			{
				object value;
				if (TryEvaluate(argument, out value))
					argsList.Add(value);
				else
					throw new InvalidOperationException("The expression used for this argument is too complex.  Please try to simplify.  " + argument);
			}

			return argsList.ToArray();
		}

		//Method from Marc Gravell
		//http://stackoverflow.com/questions/2616638/access-the-value-of-a-member-expression
		//http://code.google.com/p/protobuf-net/source/browse/trunk/protobuf-net.Extensions/ServiceModel/Client/ProtoClientExtensions.cs
		private static bool TryEvaluate(Expression operation, out object value)
		{
			if (operation == null)
			{   // used for static fields, etc
				value = null;
				return true;
			}
			switch (operation.NodeType)
			{
				case ExpressionType.Constant:
					value = ((ConstantExpression)operation).Value;
					return true;
				case ExpressionType.MemberAccess:
					MemberExpression me = (MemberExpression)operation;
					object target;
					if (TryEvaluate(me.Expression, out target))
					{ // instance target
						switch (me.Member.MemberType)
						{
							case MemberTypes.Field:
								value = ((FieldInfo)me.Member).GetValue(target);
								return true;
							case MemberTypes.Property:
								value = ((PropertyInfo)me.Member).GetValue(target, null);
								return true;
						}
					}
					break;
				default:
					//value = Expression.Lambda(operation).Compile().DynamicInvoke();
					Func<object> d = Expression.Lambda<Func<object>>(operation).Compile();
					value = d();
					return true;
			}

			value = null;
			return false;
		}


		#endregion

		#region SetProperty

		public virtual IAutoValueSyntax<TProperty> SetProperty<TProperty>(Func<T, TProperty> function)
		{
			bool isIndexer = false;

			using (InterfaceMessageGrabber<T> grabber = new InterfaceMessageGrabber<T>())
			{
				function(grabber.Target);

				MethodCall call = grabber.Calls[grabber.Calls.Count - 1];
				MethodBase method = call.MethodBase;

				if (!method.IsSpecialName)
					throw new InvalidOperationException("This method expects a property, not a method, or event.");

				string name = method.Name.Replace("set_", string.Empty).Replace("get_", string.Empty); //get is needed if is used like SetProperty(m=>m.prop)
				IValueSyntax valueSyntax = SetProperty(name);

				MethodBase mbase;
				Type[] parameterTypes = method.GetParameters().Select(p => p.ParameterType).ToArray();
				int i = FindIndexer(typeof(T), parameterTypes, out mbase);

				//PropertyInfo propertyInfo = typeof (T).GetProperty(name, new[] {method.GetParameters()[0].ParameterType});

				//ParameterInfo[] parameters = method.GetParameters();

				//check for args
				if (i == 1)
				{
					//properties have args when used like SetProperty(m=>m.prop[1] = "d") or SetProperty(m=>m.prop[1]) or SetProperty(m=>m.prop[1,3])
					//grab all but the last one.  If the user wants to grab both then use SetPropertyTo.
					base.With(call.Args[0]);
					isIndexer = true;
				}

				return new PropertyValueBuilder<TProperty>(this, isIndexer);
			}
		}

		public virtual ICommentSyntax SetPropertyTo(Action<T> action)
		{
			using (InterfaceMessageGrabber<T> grabber = new InterfaceMessageGrabber<T>())
			{
				action(grabber.Target);

				MethodCall call = grabber.Calls[grabber.Calls.Count - 1];
				MethodBase method = call.MethodBase;
				IValueSyntax valueSyntax = SetProperty(method.Name.Replace("set_", string.Empty));

				//check for args
				if (call.ArgCount > 1)
				{
					//properties have args when used like SetPropertyTo(m=>m.prop[1] = "d")
					base.With(call.Args);
				}
				else
				{
					valueSyntax.To(call.Args[0]);
				}


				return this;
			}
		}

		#endregion

		#region GetProperty

		public IAutoMatchSyntax<TProperty> GetProperty<TProperty>(Expression<Func<T, TProperty>> expression)
		{
			string name;

			if (expression.Body is MemberExpression)
			{
				name = ((MemberExpression)expression.Body).Member.Name;
				IMatchSyntax matchSyntax = GetProperty(name);
			}
			else if (expression.Body is MethodCallExpression) //indexer
			{
				name = ((MethodCallExpression)expression.Body).Method.Name.Replace("get_", string.Empty);
				IMatchSyntax matchSyntax = GetProperty(name);

				object[] expectedArguments = ParseArguments(expression.Body as MethodCallExpression);
				//check for args
				if (expectedArguments.Length > 0)
				{
					//properties have args when used like GetProperty(m=>m.prop[1])
					base.With(expectedArguments);
				}
			}
			else
			{
				throw new InvalidOperationException("Unexpected type of expression: " + expression.Body.GetType().ToString());
			}

			return new PropertyMatchBuilder<TProperty>(this);
		}

		public ICommentSyntax GetProperty<TProperty>(Expression<Func<T, TProperty>> expression, TProperty actualValue)
		{
			IMatchSyntax matchSyntax = GetProperty(expression);
			return matchSyntax.Will(Return.Value(actualValue));
		}

		#endregion

		#region Events

		[Obsolete("Use EventBinding instead of this method.  It will infer the type of binding.")]
		public MockEventInvoker EventAdd(Action<T> action)
		{
			IMatchSyntax eventExpectation = AddEventHandlerInternal(action);
			MockEventInvoker invoker = new MockEventInvoker();
			eventExpectation.Will(invoker.Hookup());

			return invoker;
		}

		[Obsolete("Use EventBinding instead of this method.  It will infer the type of binding.")]
		public MockEventInvoker<TEventArgs> EventAdd<TEventArgs>(Action<T> action) where TEventArgs : EventArgs
		{
			IMatchSyntax eventExpectation = AddEventHandlerInternal(action);
			MockEventInvoker<TEventArgs> invoker = new MockEventInvoker<TEventArgs>();
			eventExpectation.Will(invoker.Hookup());

			return invoker;
		}

		[Obsolete("Use EventBinding instead of this method.  It will infer the type of binding.")]
		public IMatchSyntax EventRemove(Action<T> action)
		{
			return RemoveEventHandlerInternal(action);
		}

		#endregion

		#region New Event methods

		public MockEventInvoker EventBinding(Action<T> action)
		{
			IMatchSyntax eventExpectation;
			MethodBase methodBase = GetActionName(action);
			string name = methodBase.Name;

			if (name.StartsWith("add_"))
			{
				eventExpectation = EventAdd(name.Replace("add_", ""), Is.Anything);
			}
			else
			{
				eventExpectation = EventRemove(name.Replace("remove_", ""), Is.Anything);
			}

			if (methodBase.GetParameters()[0].ParameterType == typeof(EventHandler<EventArgs>))
			{
				//we are being polite here and allowing this hookup.
				MockEventInvoker<EventArgs> invoker = new MockEventInvoker<EventArgs>();
				eventExpectation.Will(invoker.Hookup());

				return new MockEventInvoker();
			}
			else
			{
				MockEventInvoker invoker = new MockEventInvoker();
				eventExpectation.Will(invoker.Hookup());

				return invoker;
			}
		}

		public MockEventInvoker<TEventArgs> EventBinding<TEventArgs>(Action<T> action) where TEventArgs : EventArgs
		{
			IMatchSyntax eventExpectation;
			MethodBase methodBase = GetActionName(action);
			string name = methodBase.Name;

			if (name.StartsWith("add_"))
			{
				eventExpectation = EventAdd(name.Replace("add_", ""), Is.Anything);
			}
			else
			{
				eventExpectation = EventRemove(name.Replace("remove_", ""), Is.Anything);
			}

			MockEventInvoker<TEventArgs> invoker = new MockEventInvoker<TEventArgs>();
			eventExpectation.Will(invoker.Hookup());

			return invoker;
		}


		#endregion

		#region Private

		private IMatchSyntax AddEventHandlerInternal(Action<T> action)
		{
			string name = GetActionName(action).Name;
			string eventName = name.Replace("add_", "");

			IMatchSyntax eventExpactation = EventAdd(eventName, Is.Anything);
			return eventExpactation;
		}

		private IMatchSyntax RemoveEventHandlerInternal(Action<T> action)
		{
			string name = GetActionName(action).Name;
			string eventName = name.Replace("remove_", "");

			IMatchSyntax eventExpactation = EventRemove(eventName, Is.Anything);
			return eventExpactation;
		}

		private MethodBase GetActionName(Action<T> action)
		{
			//TODO: more exception handling
			byte[] methodbody = action.Method.GetMethodBody().GetILAsByteArray(); //I love you metadata
			int token = BitConverter.ToInt32(methodbody, 3); //I love you tokens
			MethodBase mbase = action.Method.Module.ResolveMethod(token);  //I love you reflection

			return mbase;
		}

		public void AddArgumentMatcher(Matcher matcher)
		{
			((ArgumentsMatcher)base.expectation.ArgumentsMatcher).AddMatcher(matcher);
		}

		//from DLinq library
		protected int FindIndexer(Type type, Type[] args, out MethodBase method)
		{
			foreach (Type t in SelfAndBaseTypes(type))
			{
				MemberInfo[] members = t.GetDefaultMembers();
				if (members.Length != 0)
				{
					IEnumerable<PropertyInfo> properties = members.
						OfType<PropertyInfo>().
						Select(p => p)
						.Where(p => p != null && p.GetGetMethod(true) != null && p.GetSetMethod(true) != null && p.GetGetMethod(true).GetParameters().Count() + 1 == p.GetSetMethod(true).GetParameters().Count());

					IEnumerable<MethodBase> methods = properties.Select(p => (MethodBase)p.GetGetMethod(true)).Where(m => m != null);

					int count = FindBestMethod(methods, args, out method);

					if (count == 0)
					{
						methods = properties.Select(p => (MethodBase)p.GetSetMethod(true)).Where(m => m != null);

						count = FindBestMethod(methods, args, out method);
					}

					if (count != 0)
						return count;
				}
			}
			method = null;
			return 0;
		}

		static IEnumerable<Type> SelfAndBaseTypes(Type type)
		{
			if (type.IsInterface)
			{
				List<Type> types = new List<Type>();
				AddInterface(types, type);
				return types;
			}
			return SelfAndBaseClasses(type);
		}
		static IEnumerable<Type> SelfAndBaseClasses(Type type)
		{
			while (type != null)
			{
				yield return type;
				type = type.BaseType;
			}
		}

		static void AddInterface(List<Type> types, Type type)
		{
			if (!types.Contains(type))
			{
				types.Add(type);
				foreach (Type t in type.GetInterfaces()) AddInterface(types, t);
			}
		}
		class MethodData
		{
			public MethodBase MethodBase;
			public ParameterInfo[] Parameters;
			public Type[] Args;
		}

		int FindBestMethod(IEnumerable<MethodBase> methods, Type[] args, out MethodBase method)
		{
			MethodData[] applicable = methods.
				Select(m => new MethodData { MethodBase = m, Parameters = m.GetParameters() }).
				Where(m => IsApplicable(m, args)).
				ToArray();
			if (applicable.Length > 1)
			{
				applicable = applicable.
					Where(m => applicable.All(n => m == n || IsBetterThan(args, m, n))).
					ToArray();
			}
			if (applicable.Length == 1)
			{
				MethodData md = applicable[0];
				for (int i = 0; i < args.Length; i++) args[i] = md.Args[i];
				method = md.MethodBase;
			}
			else
			{
				method = null;
			}
			return applicable.Length;
		}

		bool IsApplicable(MethodData method, Type[] args)
		{
			if (method.Parameters.Length != args.Length)
				return false;

			Type[] promotedArgs = new Type[args.Length];
			for (int i = 0; i < args.Length; i++)
			{
				ParameterInfo pi = method.Parameters[i];
				if (pi.IsOut) return false;
				Type promoted = PromoteExpression(args[i], pi.ParameterType, false);
				if (promoted == null)
					return false;
				promotedArgs[i] = promoted;
			}
			method.Args = promotedArgs;
			return true;
		}

		Type PromoteExpression(Type expr, Type type, bool exact)
		{
			if (expr == type)
				return expr;

			if (IsCompatibleWith(expr, type))
			{
				//if (type.IsValueType || exact) 
				//return Expression.Convert(expr, type);
				return expr;
			}
			return null;
		}


		static bool IsBetterThan(Type[] args, MethodData m1, MethodData m2)
		{
			bool better = false;
			for (int i = 0; i < args.Length; i++)
			{
				int c = CompareConversions(args[i],
					m1.Parameters[i].ParameterType,
					m2.Parameters[i].ParameterType);
				if (c < 0) return false;
				if (c > 0) better = true;
			}
			return better;
		}

		// Return 1 if s -> t1 is a better conversion than s -> t2
		// Return -1 if s -> t2 is a better conversion than s -> t1
		// Return 0 if neither conversion is better
		static int CompareConversions(Type s, Type t1, Type t2)
		{
			if (t1 == t2) return 0;
			if (s == t1) return 1;
			if (s == t2) return -1;
			bool t1t2 = IsCompatibleWith(t1, t2);
			bool t2t1 = IsCompatibleWith(t2, t1);
			if (t1t2 && !t2t1) return 1;
			if (t2t1 && !t1t2) return -1;
			if (IsSignedIntegralType(t1) && IsUnsignedIntegralType(t2)) return 1;
			if (IsSignedIntegralType(t2) && IsUnsignedIntegralType(t1)) return -1;
			return 0;
		}
		static bool IsCompatibleWith(Type source, Type target)
		{
			if (source == target) return true;
			if (!target.IsValueType) return target.IsAssignableFrom(source);
			Type st = GetNonNullableType(source);
			Type tt = GetNonNullableType(target);
			if (st != source && tt == target) return false;
			TypeCode sc = st.IsEnum ? TypeCode.Object : Type.GetTypeCode(st);
			TypeCode tc = tt.IsEnum ? TypeCode.Object : Type.GetTypeCode(tt);
			switch (sc)
			{
				case TypeCode.SByte:
					switch (tc)
					{
						case TypeCode.SByte:
						case TypeCode.Int16:
						case TypeCode.Int32:
						case TypeCode.Int64:
						case TypeCode.Single:
						case TypeCode.Double:
						case TypeCode.Decimal:
							return true;
					}
					break;
				case TypeCode.Byte:
					switch (tc)
					{
						case TypeCode.Byte:
						case TypeCode.Int16:
						case TypeCode.UInt16:
						case TypeCode.Int32:
						case TypeCode.UInt32:
						case TypeCode.Int64:
						case TypeCode.UInt64:
						case TypeCode.Single:
						case TypeCode.Double:
						case TypeCode.Decimal:
							return true;
					}
					break;
				case TypeCode.Int16:
					switch (tc)
					{
						case TypeCode.Int16:
						case TypeCode.Int32:
						case TypeCode.Int64:
						case TypeCode.Single:
						case TypeCode.Double:
						case TypeCode.Decimal:
							return true;
					}
					break;
				case TypeCode.UInt16:
					switch (tc)
					{
						case TypeCode.UInt16:
						case TypeCode.Int32:
						case TypeCode.UInt32:
						case TypeCode.Int64:
						case TypeCode.UInt64:
						case TypeCode.Single:
						case TypeCode.Double:
						case TypeCode.Decimal:
							return true;
					}
					break;
				case TypeCode.Int32:
					switch (tc)
					{
						case TypeCode.Int32:
						case TypeCode.Int64:
						case TypeCode.Single:
						case TypeCode.Double:
						case TypeCode.Decimal:
							return true;
					}
					break;
				case TypeCode.UInt32:
					switch (tc)
					{
						case TypeCode.UInt32:
						case TypeCode.Int64:
						case TypeCode.UInt64:
						case TypeCode.Single:
						case TypeCode.Double:
						case TypeCode.Decimal:
							return true;
					}
					break;
				case TypeCode.Int64:
					switch (tc)
					{
						case TypeCode.Int64:
						case TypeCode.Single:
						case TypeCode.Double:
						case TypeCode.Decimal:
							return true;
					}
					break;
				case TypeCode.UInt64:
					switch (tc)
					{
						case TypeCode.UInt64:
						case TypeCode.Single:
						case TypeCode.Double:
						case TypeCode.Decimal:
							return true;
					}
					break;
				case TypeCode.Single:
					switch (tc)
					{
						case TypeCode.Single:
						case TypeCode.Double:
							return true;
					}
					break;
				default:
					if (st == tt) return true;
					break;
			}
			return false;
		}

		static bool IsSignedIntegralType(Type type)
		{
			return GetNumericTypeKind(type) == 2;
		}

		static bool IsUnsignedIntegralType(Type type)
		{
			return GetNumericTypeKind(type) == 3;
		}

		static int GetNumericTypeKind(Type type)
		{
			type = GetNonNullableType(type);
			if (type.IsEnum) return 0;
			switch (Type.GetTypeCode(type))
			{
				case TypeCode.Char:
				case TypeCode.Single:
				case TypeCode.Double:
				case TypeCode.Decimal:
					return 1;
				case TypeCode.SByte:
				case TypeCode.Int16:
				case TypeCode.Int32:
				case TypeCode.Int64:
					return 2;
				case TypeCode.Byte:
				case TypeCode.UInt16:
				case TypeCode.UInt32:
				case TypeCode.UInt64:
					return 3;
				default:
					return 0;
			}
		}

		static bool IsNullableType(Type type)
		{
			return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
		}

		static Type GetNonNullableType(Type type)
		{
			return IsNullableType(type) ? type.GetGenericArguments()[0] : type;
		}

		#endregion
	}
}